package objectstorage

import (
	"cmp"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"io"
	"net/http"
	"net/url"
	"slices"
	"strings"
	"time"

	"github.com/solsw/generichelper"
	"github.com/solsw/maphelper"
	"github.com/solsw/slicehelper"
)

// Генерирование подписи.
// https://cloud.ru/ru/docs/s3e/ug/topics/api__getting-started.html#id3

func hmacSHA256(key []byte, s string) []byte {
	h := hmac.New(sha256.New, key)
	h.Write([]byte(s))
	return h.Sum(nil)
}

func hexSHA256(bb []byte) string {
	h := sha256.New()
	h.Write(bb)
	return hex.EncodeToString(h.Sum(nil))
}

// SigningKey генерирует подписывающий ключ.
func SigningKey(secret, nowDate string) []byte {
	// https://cloud.ru/ru/docs/s3e/ug/topics/api__getting-started.html#getting-started-signingkey
	// DateKey = HMAC-SHA256("AWS4"+"<KeySecret>", "<YYYYMMDD>")
	DateKey := hmacSHA256([]byte("AWS4"+secret), nowDate)
	// DateRegionKey = HMAC-SHA256(DateKey, "ru-central-1")
	DateRegionKey := hmacSHA256(DateKey, "ru-central-1")
	// DateRegionServiceKey = HMAC-SHA256(DateRegionKey, "s3")
	DateRegionServiceKey := hmacSHA256(DateRegionKey, "s3")
	// SigningKey = HMAC-SHA256(DateRegionServiceKey, "aws4_request")
	return hmacSHA256(DateRegionServiceKey, "aws4_request")
}

func CanonicalQueryString(rawQuery string) (string, error) {
	if rawQuery == "" {
		return "", nil
	}
	q, err := url.ParseQuery(rawQuery)
	if err != nil {
		return "", err
	}
	// Encode encodes the values into “URL encoded” form ("bar=baz&foo=quux") sorted by key.
	return q.Encode(), nil
}

func CanonicalAndSignedHeaders(header http.Header, headerNames []string) (string, string, error) {
	if len(header) == 0 {
		return "", "", errors.New("empty header")
	}
	if len(headerNames) == 0 {
		return "", "", errors.New("empty headerNames")
	}
	kk := slicehelper.Map[[]string, []generichelper.Tuple2[string, string]](
		maphelper.Keys(header),
		func(k string) generichelper.Tuple2[string, string] {
			return generichelper.Tuple2[string, string]{Item1: strings.ToLower(k), Item2: k}
		})
	loKeysToSign := slicehelper.Map[[]string, []string](headerNames, func(k string) string { return strings.ToLower(k) })
	kk = slices.DeleteFunc(kk, func(k generichelper.Tuple2[string, string]) bool {
		return !slices.Contains(loKeysToSign, k.Item1)
	})
	if len(kk) == 0 {
		return "", "", errors.New("no keysToSign in header")
	}
	slices.SortFunc(kk, func(a, b generichelper.Tuple2[string, string]) int { return cmp.Compare(a.Item1, b.Item1) })
	hh := make([]string, 0, len(kk))
	for _, k := range kk {
		for _, v := range header[k.Item2] {
			hh = append(hh, k.Item1+":"+v)
		}
	}
	canonicalHeaders := strings.Join(hh, "\n")
	signedHeaders := strings.Join(
		slicehelper.Map[[]generichelper.Tuple2[string, string], []string](kk,
			func(k generichelper.Tuple2[string, string]) string { return k.Item1 }),
		";")
	return canonicalHeaders, signedHeaders, nil
}

func HashedPayload(body io.ReadCloser) (string, error) {
	if body == nil {
		return hexSHA256([]byte("")), nil
	}
	rscBody, ok := body.(io.ReadSeekCloser)
	if !ok {
		return "", errors.New("body is not io.ReadSeekCloser")
	}
	bb, err := io.ReadAll(rscBody)
	if err != nil {
		return "", err
	}
	_, err = rscBody.Seek(0, io.SeekStart)
	if err != nil {
		return "", err
	}
	if len(bb) == 0 {
		return hexSHA256([]byte("")), nil
	}
	return hexSHA256(bb), nil
}

// CanonicalRequest создаёт канонический запрос.
func CanonicalRequest(r *http.Request, headerNames []string) (string, string, error) {
	// https://cloud.ru/ru/docs/s3e/ug/topics/api__getting-started.html#getting-started-canonicalrequest
	canonicalQueryString, err := CanonicalQueryString(r.URL.RawQuery)
	if err != nil {
		return "", "", err
	}
	canonicalHeaders, signedHeaders, err := CanonicalAndSignedHeaders(r.Header, headerNames)
	if err != nil {
		return "", "", err
	}
	hashedPayload, err := HashedPayload(r.Body)
	if err != nil {
		return "", "", err
	}
	canonicalRequest := strings.Join([]string{
		r.Method,
		r.URL.Path,
		canonicalQueryString,
		canonicalHeaders,
		signedHeaders,
		hashedPayload,
	}, "\n")
	return canonicalRequest, signedHeaders, nil
}

// StringToSign генерирует строку для подписи.
func StringToSign(r *http.Request, headerNames []string, nowDate, nowTime string) (string, string, error) {
	// https://cloud.ru/ru/docs/s3e/ug/topics/api__getting-started.html#getting-started-stringtosign
	canonicalRequest, signedHeaders, err := CanonicalRequest(r, headerNames)
	if err != nil {
		return "", "", err
	}
	stringToSign := strings.Join([]string{
		"AWS4-HMAC-SHA256",
		nowDate + "T" + nowTime + "Z",
		nowDate + "/ru-central-1/s3/aws4_request",
		hexSHA256([]byte(canonicalRequest)),
	}, "\n")
	return stringToSign, signedHeaders, nil
}

func Signature(secret string, r *http.Request, headerNames []string) (string, string, string, error) {
	// https://cloud.ru/ru/docs/s3e/ug/topics/api__getting-started.html#getting-started-signature
	now := time.Now()
	nowDate := now.Format("20060102")
	nowTime := now.Format("150405")
	signingKey := SigningKey(secret, nowDate)
	stringToSign, signedHeaders, err := StringToSign(r, headerNames, nowDate, nowTime)
	if err != nil {
		return "", "", "", err
	}
	// Signature = HMAC-SHA256(SigningKey, StringToSign)
	signature := hex.EncodeToString(hmacSHA256(signingKey, stringToSign))
	return signature, signedHeaders, nowDate, nil
}

func Authorization(r *http.Request, headerNames []string, keyId, secret, tenantId string) (string, error) {
	// https://cloud.ru/ru/docs/s3e/ug/topics/api__getting-started.html#http-authorization
	signature, signedHeaders, nowDate, err := Signature(secret, r, headerNames)
	if err != nil {
		return "", err
	}
	return "AWS4-HMAC-SHA256 Credential=" +
			tenantId + ":" + keyId + "/" + nowDate + "/ru-central-1/s3/aws4_request," +
			"SignedHeaders=" + signedHeaders + "," +
			"Signature=" + signature,
		nil
}
